;代码功能描述:
;本例中 8255的C口做输出 C接在8个LED上 
;通过键盘控制显示方式 使用循环右移运算 循环左移运算 改变1在显示码中的位置 
;将LED显示码通过C口送入LED中 从而实现了循环左(右)亮灯的效果
;此外通过取反来实现了 间隔闪烁(轮替亮灭灯)的效果

;简注:本代码中应用了两种解决条件跳转范围不足的方案 应当加以积累
;此外本代码用6号功能做用户输入的实时检测 堪称经典 在实验中本人大受启发
;此外 原始代码中左右循环置的初态都是高7位灯亮最低位灯灭(0FEH)
;     实现的效果是每次朝某个方向循环灭一个灯 其余灯亮
;笔者在实验中将其修改成 每次朝一个方向亮一个灯 其余灯灭
;因此实际设置的初值为:左循环(0000 0001)01H | 右循环(1000 0000)80H 
DATA SEGMENT
;-接口区-
IO8255C EQU 28AH              ;8255 PC口
IO8255D EQU 28BH              ;8255 控制端口 
;-提示信息区-
MESS1 DB 'Press 1,left loop ',0DH,0AH     ;左循环闪烁
      DB 'Press 2,right loop',0DH,0AH     ;右循环闪烁
      DB 'Press 3,discontinious',0DH,0AH  ;不连续的间断闪烁
      DB 'Press 4,quit',0DH,0AH,'$'
MESS2 DB 'Wrong key!',0DH,0AH,'$'
MESS3 DB 0DH,0AH,'Press any to come back Menu.',0DH,0AH,'$'
DATA ENDS

CODE SEGMENT
ASSUME CS:CODE,DS:DATA 
START:
;初始化设置
      ;数据段初始化
      MOV AX,DATA 
      MOV DS,AX
      ;向8255控制端口写80H(1000 0000) 
      ;(1)特征位 (00)A方式0 (0)A输出 
      ;(0)C高4位输出 (0)B方式0 (0)B输出 (0)C低4位输出
      ;即ABC均做输出 工作方式为0
      MOV DX,IO8255D
      MOV AL,80H
      OUT DX,AL 

;BG:  显示功能菜单 并根据用户按键跳转指定分支
      ;显示菜单
BG:   LEA DX,MESS1 
      MOV AH ,9 
      INT 21H      
      ;此处8号功能 输入字符到AL 无回显 
      ;区别2号功能 输入字符到AL 有回显
      MOV AH,8 
      INT 21H          ;此处不回显 意味着按键即生效 而非是输入字符生效
      ;返回菜单提示
      PUSH AX         ;保护AL
      LEA DX,MESS3    ;提示用户按任意键返回 与后方的6号功能首尾呼应!
      MOV AH,9
      INT 21H 
      POP AX           ;恢复AL
      ;根据用户按下的键 确定执行的分支
      CMP AL,'1' 
      JZ ZXH            ;按下1 左循环闪烁
      CMP AL,'2'        
      JZ YXH            ;按下2 右循环闪烁
      CMP AL,'3' 
      JZ JGS            ;按下3 间隔闪烁
      CMP AL,'4' 
      ;此处没用JZ QIT 是因为JZ跳转范围有限
      ;当代码较长时 JZ失效 因此需要JMP
      ;故而此处先用JNZ跳转近处不等于4的情况
      ;后面再跟上跳转范围更大但没有条件检测功能的JMP
      ;应当积累这种解决方案
      JNZ WX            ;不是4 提示错误信息
      JMP QIT           ;是4 则退出


;WX: 提示用户按键错误 并返回BG重来
WX:   LEA DX,MESS2 
      MOV AH,09H 
      INT 21H 
      JMP BG 

;ZXH  左循环闪烁功能模块
      ;向8255的C口写入01H(0000 0001) 
      ;设置LED灯初态为只有最右侧亮一灯
ZXH:  MOV DX,IO8255C 
      MOV AL,01H
BB:   OUT DX,AL
      ;闪烁间隔时间(延迟) 
      MOV CX,0FFFFH 
L1:   LOOP L1 
      ;将AL中的数循环左移 获得下一个LED显示态对应的数值
      ROL AL,1 ;循环位移的好处在于不会丢失数中的1 从而实现循环亮灯
      ;检测是否有用户输入 若有返回菜单 不等待用户!
      PUSH AX     ;保护AL(LED显示态)和DX(C端口地址) 
      PUSH DX 
      MOV DL,0FFH ;DL=0FFH(输入请求)
      MOV AH,06H  ;6号功能(直接控制台I/O) 
      INT 21H     ;尝试从标准输入中获取用户输入到AL
                  ;若用户自上次输入后再无输入 则返回0到AL
      JNZ BG      ;AL不为0 则跳BG重来
                  ;此处跳转与 BG模块下的"按任意键返回菜单"首尾呼应!
      POP DX      ;恢复DX(C端口地址)和AL(LED显示态)
      POP AX 
      JMP BB      ;写C端口 更新LED的显示状态


;YXH: 右循环闪烁模块
      ;向8255的C口写入01H(1000 0000) 
      ;设置LED灯初态为只有最左侧亮一灯
YXH:  MOV DX,IO8255C
      MOV AL,80H 
EE:   OUT DX,AL 
      ;闪烁间隔时间(延迟)
      MOV CX,0FFFFH 
L2:   LOOP L2
      ;将AL中的数循环右移 获得下一个LED显示态对应的数值
      ROR AL,1 
      ;检测是否有用户输入 若有返回菜单若无继续循环亮灯 详细解释同ZXH
      PUSH AX 
      PUSH DX      ;保护AL(LED显示态)和DX(C端口地址) 
      MOV DL,0FFH 
      MOV AH,06H 
      INT 21H     ;尝试从标准输入中获取用户输入到AL
      JNZ BG      ;AL不为0 则跳BG重来
      POP DX 
      POP AX      ;恢复DX(C端口地址)和AL(LED显示态)
      JMP EE      ;写C端口 更新LED的显示状态

;JGS: 间隔闪烁功能模块
      ;向8255的C口写入55H(0101 0101) 
      ;设置LED灯初态为亮灭相互间隔
JGS:  MOV DX,IO8255C 
      MOV AL,55H 
HH:   OUT DX,AL 
      ;闪烁间隔时间(延迟)
      MOV CX,0FFFFH 
L3:   LOOP L3 
      ;将AL取反 从而使亮灭的灯互换
      NOT AL        ;上一轮亮的灯灭掉，灭的灯亮起来
      ;检测是否有用户输入 若有返回菜单若无继续循环亮灯 详细解释同ZXH      
      PUSH AX 
      PUSH DX     ;保护AL(LED显示态)和DX(C端口地址) 
      MOV DL,0FFH 
      MOV AH,06H 
      INT 21H     ;尝试从标准输入中获取用户输入到AL
      JNZ BG1     ;AL不为0 则跳BG重来(此处不用JNZ BG理由见BG1)
      POP DX 
      POP AX      ;恢复DX(C端口地址)和AL(LED显示态)
      JMP HH      ;写C端口 更新LED的显示状态


;BG1: 中继BG跳转的指令 
      ;正如前述讨论的一样 JZ JNZ跳转范围有限
      ;因此常用的另一种方案是中继跳转
      ;让JZ或JNZ跳转到附近的一个位置
      ;在该位置由JMP进行长距离跳转
      ;这样就实现了长距离的条件跳转       
      ;这种解决方案应当积累起来
BG1:  JMP BG 

;QIT: 结束程序
QIT:  MOV AH,4CH 
      INT 21H 
CODE ENDS
END START